// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace SnapObjects.Data
{
    internal class AdoDbResultSet<TModel> : IAdoDbResultSet<TModel>
    {
        protected IAdoDbDataVisitor _dataVisitor;
        protected IDataModelBinder<TModel> _dataBinder;

        public AdoDbResultSet(IAdoDbDataVisitor dataVisitor, ITypeMapper typeMapper)
        {
            _dataVisitor = dataVisitor;

            try
            {
                var columnSchema = _dataVisitor.GetColumnSchema();

                if (columnSchema.Count > 0)
                {
                    _dataBinder = DataModelBinderFactory.Current
                        .GetBinder<TModel>(columnSchema, typeMapper);
                }
            }
            catch
            {
                _dataVisitor.Dispose();

                throw;
            }
        }

        public AdoDbResultSet(
            IAdoDbDataVisitor dataVisitor, ITypeMapper typeMapper, Type modelType)
        {
            _dataVisitor = dataVisitor;

            try
            {
                var columnSchema = _dataVisitor.GetColumnSchema();

                if (columnSchema.Count > 0)
                {
                    _dataBinder = (IDataModelBinder<TModel>)DataModelBinderFactory.Current
                        .GetBinder(columnSchema, typeMapper, modelType);
                }
            }
            catch
            {
                _dataVisitor.Dispose();

                throw;
            }
        }

        ~AdoDbResultSet()
        {
            this.Dispose();
        }

        public IModelFactory<TModel> ModelFactory => _dataBinder.ModelFactory;

        public IModelAccessor<TModel> ModelAccessor => this.ModelFactory.GetAccessor();

        public TModel Current { get; private set; }

        public bool ContainsListCollection => false;

        object IEnumerator.Current => this.Current;

        public IEnumerator<TModel> GetEnumerator()
        {
            return this;
        }

        public IList GetList()
        {
            return this.ToList();
        }

        public IList<TModel> GetList(int currentIndex, int size)
        {
            if (currentIndex < 0)
            {
                this.Dispose();
                throw new IndexOutOfRangeException("CurrentIndex should be greater than or equal to zero.");
            }

            var models = new List<TModel>();
            var index = 0;
            var ignoreData = index < currentIndex || index >= currentIndex + size;

            while (this.MoveNext(ignoreData))
            {
                if (index >= currentIndex + size)
                {
                    break;
                }

                if (!ignoreData)
                {
                    models.Add(this.Current);
                }

                index++;
                ignoreData = index < currentIndex || index >= currentIndex + size;
            }

            this.Dispose();

            return models;
        }

        public async Task<IList<TModel>> GetListAsync(
            int currentIndex, int size, CancellationToken cancellationToken = default)
        {
            if (currentIndex < 0)
            {
                this.Dispose();
                throw new IndexOutOfRangeException("CurrentIndex should be greater than or equal to zero.");
            }

            var models = new List<TModel>();
            var index = 0;
            var ignoreData = index < currentIndex || index >= currentIndex + size;

            while (!cancellationToken.IsCancellationRequested &&
                await this.MoveNextAsync(ignoreData, cancellationToken))
            {
                if (index >= currentIndex + size)
                {
                    break;
                }

                if (!ignoreData)
                {
                    models.Add(this.Current);
                }

                index++;
                ignoreData = index < currentIndex || index >= currentIndex + size;
            }

            await this.DisposeAsync();

            return models;
        }

        public bool MoveNext()
        {
            return this.MoveNext(false);
        }

        private bool MoveNext(bool ignoreData)
        {
            if (_dataVisitor.MoveNext())
            {
                if (ignoreData)
                {
                    this.Current = default;
                }
                else
                {
                    var model = _dataBinder.BindModel(_dataVisitor.ReadData());
                    this.Current = model;
                }

                return true;
            }
            else
            {
                return false;
            }
        }

        public ValueTask<bool> MoveNextAsync(CancellationToken cancellationToken)
        {
            return this.MoveNextAsync(false, cancellationToken);
        }

        public void Reset()
        {
            throw new NotSupportedException("ADO.NET DbDataReader does not support reset operations.");
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        public IList<DbColumnInfo> GetColumnSchema()
        {
            return _dataVisitor.GetColumnSchema();
        }

        public void Dispose()
        {
            _dataVisitor.Dispose();
        }

        private CancellationToken _cancellationToken;

        public IAsyncEnumerator<TModel> GetAsyncEnumerator(CancellationToken cancellationToken = default)
        {
            _cancellationToken = cancellationToken;

            return this;
        }

        public async ValueTask<bool> MoveNextAsync()
        {
            return await this.MoveNextAsync(false, _cancellationToken);
        }

        public async ValueTask DisposeAsync()
        {
            await _dataVisitor.DisposeAsync();
        }

        private async ValueTask<bool> MoveNextAsync(bool ignoreData, CancellationToken cancellationToken)
        {
            if (await _dataVisitor.MoveNextAsync(cancellationToken))
            {
                if (ignoreData)
                {
                    this.Current = default;
                }
                else
                {
                    var model = _dataBinder.BindModel(_dataVisitor.ReadData());

                    this.Current = model;
                }

                return true;
            }
            else
            {
                return false;
            }
        }
    }
}
